﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;

using NLog;

using Sonic.Framework.Site;
using Sonic.Framework.Extensions;
using Sonic.Framework.Enums;
using Sonic.Framework.Interfaces;
using System.Text.RegularExpressions;

namespace Sonic.Framework
{
    /// <summary>
    /// 
    /// TODO LIST:
    /// - stats collection (events?)
    /// - error reporting
    /// - license header at top of classes
    /// - fix svn
    /// - update file references
    /// - Semaphore and synch
    /// - interface design
    /// - do the logging properly
    /// 
    /// </summary>
    public class SonicProcessor : ISonicProcessor
    {
        private string[] contentFileExtensions = new string[] { ".htm", ".html", ".php", ".aspx", ".xslt", ".xml", ".js", ".css" };

        private Stopwatch _stopwatch = new Stopwatch();

        public string LoggerName
        {
            set
            {
                NLogSingleton.SetLoggerName(value);
            }
        }

        /// <summary>
        /// An instance of a logger for writing out debug information
        /// </summary>
        public Logger Logger
        {
            get {
                return NLogSingleton.Instance.Logger;
            }
        }

        /// <summary>
        /// A list of site profiles to be processed by sonic
        /// </summary>
        private List<ISiteProfile> _sites = new List<ISiteProfile>();
        public List<ISiteProfile> Sites
        {
            get
            {
                return _sites;
            }
            set
            {
                _sites = value;
            }
        }

        /// <summary>
        /// A list of tools to be available for the processor when processing site profiles
        /// </summary>
        private Dictionary<string, IProcessorTool> _processorTools = new Dictionary<string, IProcessorTool>();
        public Dictionary<string, IProcessorTool> ProcessorTools
        {
            get
            {
                return _processorTools;
            }
            set
            {
                _processorTools = value;
            }
        }

        /// <summary>
        /// A list of plugin processor tool directives. These directives will allow for custom actions to be performed pre and post
        /// ProcessorTool execution. They can be used to filter input, or modify output.
        /// </summary>
        private Dictionary<string, IProcessorToolDirective> _processorToolDirectives = new Dictionary<string, IProcessorToolDirective>();
        public Dictionary<string, IProcessorToolDirective> ProcessorToolDirectives
        {
            get
            {
                return _processorToolDirectives;
            }
            set
            {
                _processorToolDirectives = value;
            }
        }

        /// <summary>
        /// File references to be updated in content within the site after all the processor tools and directives have
        /// executed.
        /// </summary>
        private List<IFileReferenceUpdate> _fileReferenceUpdates = new List<IFileReferenceUpdate>();
        public List<IFileReferenceUpdate> FileReferenceUpdates
        {
            get
            {
                return _fileReferenceUpdates;
            }
        }

        public void AddFileReferenceUpdate(IFileReferenceUpdate update)
        {
            this._fileReferenceUpdates.Add(update);

            // TODO: If there is a reference update that uses this reference update's files
            // then we should update those references to match this one (last in takes precedence)

            // file.css => file-a.css
            // a.css + file.css => combined.css

            // No point in updating file to file-a.css, as it's been combined into combined.css
        }

        /// <summary>
        /// Process each site as defined in the config XML used to build this processor
        /// </summary>
        public void Process()
        {
            _stopwatch.Start();

            foreach (SiteProfile site in Sites)
            {
                ProcessSite(site);
            }

            // TODO: Move into thread safe
            _stopwatch.Stop();
            long timeTakenMs = _stopwatch.ElapsedMilliseconds;

            Logger.Info("Processed " + Sites.Count + " sites in " + timeTakenMs + " milliseconds");
        }

        /// <summary>
        /// Process a site profile's directories and update file references
        /// </summary>
        /// <param name="site"></param>
        private void ProcessSite(SiteProfile site)
        {
            Logger.Info("Processing site: " + site.RootDirectoryInfo.FullName);

            foreach (SiteDirectory dir in site.Directories)
            {
                ProcessDirectory(dir);
            }

            // Update changed file names in content within the site
            UpdateFileReferencesInSiteContent(site.RootDirectoryInfo, site.RootDirectoryInfo, 0);
        }

        /// <summary>
        /// Process a site directory, executing the processor tool and directives over itself
        /// </summary>
        /// <param name="dir"></param>
        private void ProcessDirectory(SiteDirectory dir)
        {
            Logger.Info("> Processing directory: " + dir.DirectoryInfo.FullName + " ...");

            // If directory config is invalid output an explaination of why
            if (dir.Tool == null || !dir.DirectoryInfo.Exists)
            {
                if (dir.Tool == null || !dir.Tool.ExecutablePath.Exists)
                {
                    Logger.Error("Processor tool not set");
                }

                if (!dir.DirectoryInfo.Exists)
                {
                    Logger.Error("Directory not found.");
                }
                return;
            }
            if (dir.Tool.Name != "Null" && !dir.Tool.ExecutablePath.Exists)
            {
                Logger.Error("Could not find processor tool exe " + dir.Tool.ExecutablePath.FullName);
                return;
            }

            // Get the files that we are going to process
            List<FileInfo> directoryFilesToBeProcessed = dir.GetMatchingFiles(ExclusionType.None);

            // Run plugged in Pre ProcessorToolDirectives
            foreach (ProcessorToolDirectiveCall call in dir.PreProcessorDirectives)
            {
                if (dir.Exclusions.Count > 0)
                    directoryFilesToBeProcessed = dir.GetMatchingFiles(ExclusionType.ProcessorToolDirective, call.name);

                Logger.Info(">> Pre " + call.processorToolDirective.Name);
                call.processorToolDirective.Execute((ISonicProcessor)this, dir, ref directoryFilesToBeProcessed, call.arguments);
            }

            // Get the list of files to process, with processor tool exclusions
            if (dir.Exclusions.Count > 0)
                directoryFilesToBeProcessed = dir.GetMatchingFiles(ExclusionType.ProcessorTool);

            // Run the ProcessorTool over a valid directory
            dir.Tool.Run(directoryFilesToBeProcessed);

            // Run plugged in Post ProcessorToolDirectives
            foreach (ProcessorToolDirectiveCall call in dir.PostProcessorDirectives)
            {
                if (dir.Exclusions.Count > 0)
                    directoryFilesToBeProcessed = dir.GetMatchingFiles(ExclusionType.ProcessorToolDirective, call.name);

                Logger.Info(">> Post " + call.processorToolDirective.Name);
                call.processorToolDirective.Execute((ISonicProcessor)this, dir, ref directoryFilesToBeProcessed, call.arguments);
            }
        }

        /// <summary>
        /// Search the content of the site for out of date file references (files that have been renamed, moved, combined) and
        /// update them to the new location
        /// </summary>
        /// <param name="referenceUpdate"></param>
        private void UpdateFileReferencesInSiteContent(DirectoryInfo rootDirectory, DirectoryInfo directory, int depth)
        {
            foreach (FileInfo file in directory.GetFiles("*.*", SearchOption.TopDirectoryOnly))
            {
                // Only process content file types
                if (contentFileExtensions.Contains(file.Extension))
                {
                    UpdateFileReferencesInFile(file, rootDirectory, directory, depth);
                }
            }

            foreach (DirectoryInfo childDirectory in directory.GetDirectories())
            {
                UpdateFileReferencesInSiteContent(rootDirectory, childDirectory, depth++);
            }
        }

        private void UpdateFileReferencesInFile(FileInfo file, DirectoryInfo rootDirectory, DirectoryInfo directory, int depth)
        {
            StreamReader reader = null;
            StreamWriter writer = null;

            try
            {
                // Read in the file contents
                reader = new StreamReader(file.FullName);
                string content = reader.ReadToEnd();
                reader.Close();

                bool contentHasChanged = false;

                // Update any references
                foreach (FileReferenceUpdate update in FileReferenceUpdates)
                {
                    string newAbsolutePath = BuildAbsoluteFilePath(update.To, rootDirectory);
                    string newRelativePath = BuildRelativeFilePath(update.To, rootDirectory);

                    foreach (FileInfo from in update.From)
                    {
                        string absolutePath = BuildAbsoluteFilePath(from, rootDirectory);
                        string relativePath = BuildRelativeFilePath(from, directory);

                        // Build up Regex for replacements
                        string reAbsolute = string.Format(@"([\\/""]+){0}({1})(\??){0}", "{0}", SanitiseFileName(absolutePath));
                        string reRelative = string.Format(@"([\\/""]+){0}((\./)?{1})(\??){0}", "{0}", SanitiseFileName(relativePath));

                        if (Regex.IsMatch(content, reAbsolute, RegexOptions.IgnoreCase))
                        {
                            contentHasChanged = true;
                            content = Regex.Replace(content, reAbsolute, newAbsolutePath, RegexOptions.IgnoreCase);
                        }

                        if (Regex.IsMatch(content, reRelative, RegexOptions.IgnoreCase))
                        {
                            contentHasChanged = true;
                            content = Regex.Replace(content, reRelative, newRelativePath, RegexOptions.IgnoreCase);
                        }
                    }
                }

                // Write back out the file
                if (contentHasChanged == true)
                {
                    writer = new StreamWriter(file.FullName);
                    writer.Write(content);
                    writer.Close();
                }
            }
            catch (UnauthorizedAccessException)
            {
                Logger.Trace("Unauthorised access for " + file.FullName);
            }
            catch (Exception ex)
            {
                Logger.ErrorException("Error replacing string in file " + file.FullName, ex);
            }
            finally
            {
                if (reader != null) reader.Close();
                if (writer != null) writer.Close();
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="file"></param>
        /// <param name="rootDirectory"></param>
        /// <returns></returns>
        /// <remarks>Always starts with a / slash</remarks>
        private string BuildAbsoluteFilePath(FileInfo file, DirectoryInfo rootDirectory)
        {
            string path = "";
            path = file.FullName;
            path = path.Substring(rootDirectory.FullName.Length);
            path = path.Replace('\\','/'); // Forward slashes
            if (path[0] != '/') 
                path = "/" + path;
            return path;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="file"></param>
        /// <param name="rootDirectory"></param>
        /// <returns></returns>
        private string BuildRelativeFilePath(FileInfo file, DirectoryInfo relativeToDirectory)
        {
            string path = "";

            if (file.DirectoryName.ToLower().StartsWith(relativeToDirectory.FullName.ToLower()))
            {
                // C:\site\css\main.css, C:\site\ ==> css/main.css
                string t = file.DirectoryName.Substring(relativeToDirectory.FullName.Length).Replace('\\', '/');
                path = t + "/" + file.Name;
            }
            else
            {
                // C:\site\|main.css, C:\site\|css\global\home\ ==> ../../../main.css
                // C:\site\|main.css, C:\site\|css\ ==> ../main.css
                // C:\site\|main.css, C:\site\| ==> main.css
                string commonRoot = "";// = relativeToDirectory.FullName.Substring(file.DirectoryName.Length).Replace('\\', '/');
                string a = "", b = "";
                if (file.DirectoryName.Length > relativeToDirectory.FullName.Length)
                {
                    a = relativeToDirectory.FullName + "/";
                    b = file.DirectoryName + "/";
                }
                else
                {
                    b = relativeToDirectory.FullName + "/";
                    a = file.DirectoryName + "/";
                }
                // Find the common root path of both file and dir
                for (int i = 0; i < a.Length; i++)
                {
                    if (a[i] != b[i]) break;
                    commonRoot += a[i];
                }

                // Get the path back to the root
                string t = relativeToDirectory.FullName.Substring(commonRoot.Length);
                t += "/";
                t = t.Replace('\\', '/');
                string pathBackToRoot = "";
                for (int i = 0; i < t.Length; i++)
                {
                    if (t[i] == '/')
                    {
                        pathBackToRoot += "../";
                    }
                }

                path = pathBackToRoot + file.FullName.Substring(commonRoot.Length).Replace('\\', '/');
            }

            return path;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        private string SanitiseFileName(string filename)
        {
            filename = filename.Replace(".", "\\.");
            filename = filename.Replace("+", "\\+");
            filename = filename.Replace("{", "\\{");
            filename = filename.Replace("}", "\\}");
            return filename;
        }
    }
}
